﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Reflection;
using Ue.Core.FileSystems;
using Ue.Infrastructure.Caching;
using Ue.Infrastructure.Caching.VolatileProvider;
using Ue.Eagle.Runtime.FileSystems;
using Ue.Eagle.Runtime.FileSystems.Dependencies;
using Ue.Eagle.Runtime.Models;
using Ue.Eagle.Runtime.Compilers;
using Ue.Eagle.Runtime.Utility;

namespace Ue.Eagle.Runtime.Loaders
{
    /// <summary>
    /// 动态编译加载器
    /// </summary>
    public class DynamicPluginLoader : PluginLoaderBase
    {
        public static readonly string[] PluginsVirtualPathPrefixes = { "~/Modules/", "~/Themes/" };

        private readonly IBuildManager buildManager;
        private readonly IRootFolder rootFolder;
        private readonly IFileMonitor virtualPathMonitor;
        private readonly IHostEnvironment hostEnvironment;
        private readonly IAssemblyProbingFolder assemblyProbingFolder;
        private readonly IDependenciesFolder dependenciesFolder;
        private readonly IProjectFileParser projectFileParser;
        private readonly ReloadWorkaround reloadWorkaround = new ReloadWorkaround();

        public DynamicPluginLoader(
            IBuildManager buildManager,
            IRootFolder rootFolder,
            IFileMonitor virtualPathMonitor,
            IHostEnvironment hostEnvironment,
            IAssemblyProbingFolder assemblyProbingFolder,
            IDependenciesFolder dependenciesFolder,
            IProjectFileParser projectFileParser)
            : base(dependenciesFolder) {

            this.buildManager = buildManager;
            this.rootFolder = rootFolder;
            this.virtualPathMonitor = virtualPathMonitor;
            this.hostEnvironment = hostEnvironment;
            this.assemblyProbingFolder = assemblyProbingFolder;
            this.projectFileParser = projectFileParser;
            this.dependenciesFolder = dependenciesFolder;
        }

        public bool Disabled { get; set; }

        public override int Priority { get { return 100; } }

        public override IEnumerable<PluginCompilationReference> GetCompilationReferences(DependencyDescriptor dependency) {
            yield return new PluginCompilationReference { BuildProviderTarget = dependency.VirtualPath };
        }

        public override IEnumerable<string> GetVirtualPathDependencies(DependencyDescriptor dependency) {
            // Return csproj and all .cs files
            return GetDependencies(dependency.VirtualPath);
        }

        public IEnumerable<string> GetFileHashDependencies(DependencyDescriptor dependency) {
            return GetDependencies(dependency.VirtualPath);
        }

        //public override void Monitor(PluginDescriptor descriptor, Action<IVolatileToken> monitor) {
        //    if (Disabled)
        //        return;

        //    // Monitor .csproj and all .cs files
        //    string projectPath = GetProjectPath(descriptor);
        //    if (projectPath != null) {
        //        foreach (var path in GetDependencies(projectPath)) {

        //            var token = virtualPathMonitor.WhenPathChanges(path);
        //            monitor(token);
        //            reloadWorkaround.Monitor(token);
        //        }
        //    }
        //}

        public override void PluginRemoved(PluginLoadingContext ctx, DependencyDescriptor dependency) {
        }

        public override void PluginDeactivated(PluginLoadingContext ctx, PluginDescriptor Plugin) {
        }

        public override void PluginActivated(PluginLoadingContext ctx, PluginDescriptor Plugin) {
            if (reloadWorkaround.AppDomainRestartNeeded) {
                ctx.RestartAppDomain = reloadWorkaround.AppDomainRestartNeeded;
            }
        }

        public override IEnumerable<PluginReferenceProbeEntry> ProbeReferences(PluginDescriptor descriptor) {
            if (Disabled)
                return Enumerable.Empty<PluginReferenceProbeEntry>();


            string projectPath = GetProjectPath(descriptor);
            if (projectPath == null)
                return Enumerable.Empty<PluginReferenceProbeEntry>();

            var projectFile = projectFileParser.Parse(projectPath);

            var result = projectFile.References.Select(r => new PluginReferenceProbeEntry {
                Descriptor = descriptor,
                Loader = this,
                Name = r.SimpleName,
                VirtualPath = rootFolder.GetProjectReferenceVirtualPath(projectPath, r.SimpleName, r.Path)
            });

            return result;
        }

        public override void ReferenceActivated(PluginLoadingContext context, PluginReferenceProbeEntry referenceEntry) {
            //Note: This is the same implementation as "PrecompiledPluginLoader"
            if (string.IsNullOrEmpty(referenceEntry.VirtualPath))
                return;

            string sourceFileName = rootFolder.ToPhysicalPath(referenceEntry.VirtualPath);

            // Copy the assembly if it doesn't exist or if it is older than the source file.
            bool copyAssembly =
                !assemblyProbingFolder.AssemblyExists(referenceEntry.Name) ||
                File.GetLastWriteTimeUtc(sourceFileName) > assemblyProbingFolder.GetAssemblyDateTimeUtc(referenceEntry.Name);

            if (copyAssembly) {
                context.CopyActions.Add(() => assemblyProbingFolder.StoreAssembly(referenceEntry.Name, sourceFileName));

                // We need to restart the appDomain if the assembly is loaded
                if (hostEnvironment.IsAssemblyLoaded(referenceEntry.Name)) {
                    context.RestartAppDomain = true;
                }
            }
        }

        public override Assembly LoadReference(DependencyReferenceDescriptor reference)
        {
            if (Disabled)
                return null;


            // DynamicPluginLoader has 2 types of references: assemblies from module bin directory
            // and .csproj.
            Assembly result;
            if (StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(reference.VirtualPath), ".dll"))
                result = assemblyProbingFolder.LoadAssembly(reference.Name);
            else {
                result = buildManager.GetCompiledAssembly(reference.VirtualPath);
            }

            return result;
        }

        public override PluginProbeEntry Probe(PluginDescriptor descriptor) {
            if (Disabled)
                return null;

            string projectPath = GetProjectPath(descriptor);
            if (projectPath == null)
                return null;

            var result = new PluginProbeEntry {
                Descriptor = descriptor,
                Loader = this,
                VirtualPath = projectPath,
                VirtualPathDependencies = GetDependencies(projectPath).ToList(),
            };

            return result;
        }

        protected override Plugin LoadWorker(PluginDescriptor descriptor) {
            if (Disabled)
                return null;

            string projectPath = GetProjectPath(descriptor);
            if (projectPath == null)
                return null;


            var assembly = buildManager.GetCompiledAssembly(projectPath);
            if (assembly == null)
                return null;


            return new Plugin {
                Descriptor = descriptor,
                Assembly = assembly,
                ExportedTypes = assembly.GetExportedTypes(),
            };
        }

        protected IEnumerable<string> GetDependencies(string projectPath) {
            var dependencies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            AddDependencies(projectPath, dependencies);
            return dependencies;
        }

        private void AddDependencies(string projectPath, HashSet<string> currentSet) {
            // Skip files from locations other than "~/Modules" and "~/Themes"
            if (string.IsNullOrEmpty(PrefixMatch(projectPath, PluginsVirtualPathPrefixes))) {
                return;
            }

            // Add project path
            currentSet.Add(projectPath);

            // Add source file paths
            var projectFile = projectFileParser.Parse(projectPath);
            string basePath = Path.GetDirectoryName(projectPath);
            currentSet.UnionWith(projectFile.SourceFilenames.Select(f => rootFolder.Combine(basePath, f)));

            // Add Project and Library references
            if (projectFile.References != null) {
                foreach (ReferenceDescriptor referenceDescriptor in projectFile.References.Where(reference => !string.IsNullOrEmpty(reference.Path))) {
                    string path = referenceDescriptor.ReferenceType == ReferenceType.Library
                                      ? rootFolder.GetProjectReferenceVirtualPath(projectPath, referenceDescriptor.SimpleName, referenceDescriptor.Path)
                                      : rootFolder.Combine(basePath, referenceDescriptor.Path);

                    // Normalize the virtual path (avoid ".." in the path name)
                    if (!string.IsNullOrEmpty(path)) {
                        path = rootFolder.ToToAppPath(path);
                    }

                    // Attempt to reference the project / library file
                    if (!string.IsNullOrEmpty(path) && !currentSet.Contains(path) && rootFolder.FileExists(path)) {
                        switch (referenceDescriptor.ReferenceType) {
                            case ReferenceType.Project:
                                AddDependencies(path, currentSet);
                                break;
                            case ReferenceType.Library:
                                currentSet.Add(path);
                                break;
                        }
                    }
                }
            }
        }

        private static string PrefixMatch(string virtualPath, params string[] prefixes) {
            return prefixes
                .FirstOrDefault(p => virtualPath.StartsWith(p, StringComparison.OrdinalIgnoreCase));
        }

        private string GetProjectPath(PluginDescriptor descriptor) 
        {
            string projectPath = rootFolder.Combine(descriptor.Location, descriptor.Id + ".csproj");

            if (!rootFolder.FileExists(projectPath)) {
                return null;
            }

            return projectPath;
        }

        /// <summary>
        /// We should be able to support reloading multiple version of a compiled module from
        /// a ".csproj" file in the same AppDomain. However, we are currently running into a 
        /// limitation with NHibernate getting confused when a type name is present in
        /// multiple assemblies loaded in an AppDomain.  So, until a solution is found, any change 
        /// to a ".csproj" file of an active module requires an AppDomain restart.
        /// The purpose of this class is to keep track of all .csproj files monitored until
        /// an AppDomain restart.
        /// </summary>
        internal class ReloadWorkaround {
            private readonly List<IVolatileToken> _tokens = new List<IVolatileToken>();

            public void Monitor(IVolatileToken whenProjectFileChanges) {
                lock (_tokens) {
                    _tokens.Add(whenProjectFileChanges);
                }
            }

            public bool AppDomainRestartNeeded {
                get {
                    lock (_tokens) {
                        return _tokens.Any(t => t.IsCurrent == false);
                    }
                }
            }
        }
    }
}
